﻿'    DotNetIRCD - An IRC Daemon written in VB.Net
'    Copyright (C) 2012 GeorgeBush
'
'    This program is free software: you can redistribute it and/or modify
'    it under the terms of the GNU General Public License as published by
'    the Free Software Foundation, either version 2 of the License, or
'    (at your option) any later version.
'
'    This program is distributed in the hope that it will be useful,
'    but WITHOUT ANY WARRANTY; without even the implied warranty of
'    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'    GNU General Public License for more details.
'
'    You should have received a copy of the GNU General Public License
'    along with this program. If not, see <http://www.gnu.org/licenses/>.

Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
Imports System.Text

''' <summary>
''' This Class handles an individual Client connection
''' </summary>
Public NotInheritable Class ClientConnection

    Public Property ClientSocket As TcpClient
    Public Property ClientData As New ClientDataClass
    Public ReadOnly Property Parent As BindPoint
        Get
            Return pParent
        End Get
    End Property
    Public ReadOnly Property ClientNumber As Integer
        Get
            Return pClientNumber
        End Get
    End Property

    Private pParent As BindPoint = Nothing
    Private pClientNumber As Integer = Nothing
    Private NickAccepted As Boolean = False
    Private HadUserCommand As Boolean = False
    Private ConnectDone As Boolean = False
    Private WithEvents PingTimer As New Timers.Timer(10000)
    Private ClientStoreIndex As Integer = -1

    ''' <summary>
    ''' Creates a new instance of the ClientConnection Class
    ''' </summary>
    ''' <param name="StartData">The StartData to be passed to the ClientConnection Class</param>
    Sub New(ByVal StartData As ClientStartData)

        With StartData
            ClientSocket = .MySocket
            pParent = .Parent
            pClientNumber = .ClientNumber
        End With

    End Sub

    ''' <summary>
    ''' Logs a Message to the appropriate LogTarget(s)
    ''' </summary>
    ''' <param name="MessageType">The Type of Message to Log</param>
    ''' <param name="Message">The Message to Log</param>
    Friend Sub Log(ByVal MessageType As LogTypeEnum, ByVal Message As String)
        ServerSettings.Logger.Log(MessageType, "{Client:" & pClientNumber.ToString & "}" & Message)
    End Sub

    ''' <summary>
    ''' This class holds the start data for a new client thread
    ''' </summary>
    Public NotInheritable Class ClientStartData

        ''' <summary>
        ''' The Parent (BindPoint) of this Client Class
        ''' </summary>
        Public Parent As BindPoint

        ''' <summary>
        ''' The Clients socket
        ''' </summary>
        Public MySocket As TcpClient

        ''' <summary>
        ''' The Client number for this Client Class
        ''' </summary>
        Public ClientNumber As Integer

    End Class

    ''' <summary>
    ''' This method is called when spawning a new thread to handle a Client Connection
    ''' </summary>
    Public Sub HandleClient()

        ' This try block is here to handle tear down due to a Thread Abort
        Try

            ' Show thread started message
            Log(LogTypeEnum.Debug, "Thread Started")

            ' This is the man thread loop to handle the client communication
            Do

                ' Do connection if needed
                If NickAccepted And HadUserCommand Then

                    If Not ConnectDone Then

                        ' Add this client to the list
                        SyncLock DataStore.ClientStore.Clients
                            ClientData.Socket = ClientSocket
                            DataStore.ClientStore.Clients.Add(ClientData)
                            ClientStoreIndex = DataStore.ClientStore.Clients.IndexOf(ClientData)
                        End SyncLock

                        ' Send the server connect messages
                        DoConnect()

                    End If

                End If

                ' Check to see if there is data available for us to read
                Try
                    If ClientSocket.GetStream.DataAvailable Then

                        ' There is data
                        Dim myReadBuffer(1024) As Byte
                        Dim myCompleteMessage As StringBuilder = New StringBuilder()
                        Dim numberOfBytesRead As Integer = 0

                        ' Keep receiving data till there is none left
                        Do
                            numberOfBytesRead = ClientSocket.GetStream.Read(myReadBuffer, 0, myReadBuffer.Length)
                            myCompleteMessage.AppendFormat("{0}", Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead))
                        Loop While ClientSocket.GetStream.DataAvailable

                        ' Split the data at vbCrLf
                        For Each t As String In Split(myCompleteMessage.ToString, vbCrLf)
                            If Not String.IsNullOrWhiteSpace(t) Then OnCommand(Trim(t))
                        Next

                    End If
                Catch ex As Exception

                    ' Log the error
                    Log(LogTypeEnum.Error, ex.Message)

                End Try

                ' Check if the connection is still alive
                If Not ClientSocket.Connected Then Exit Do

            Loop

            Log(LogTypeEnum.Debug, "Connection Closed.")

        Catch tEx As ThreadAbortException

            Log(LogTypeEnum.Debug, "Aborting")

            ' If we get aborted, close the client's connection
            If ClientSocket.Connected Then
                ClientSocket.GetStream.Close()
                ClientSocket.Close()
            End If

        Catch ex As Exception

            Log(LogTypeEnum.Error, "Unhandled Error: " & ex.ToString)

        End Try

    End Sub

    ''' <summary>
    ''' Sends a string of data to the client
    ''' </summary>
    ''' <param name="Data">The string to send. This sub adds vbCrLf</param>
    Public Sub SendString(ByVal Data As String)
        SendString(Data, ClientSocket)
    End Sub

    ''' <summary>
    ''' Sends a string of data to the client
    ''' </summary>
    ''' <param name="Data">The string to send. This sub adds vbCrLf</param>
    ''' <param name="Socket">The Socket to send the message through</param>
    Public Sub SendString(ByVal Data As String, ByVal Socket As TcpClient)

        Log(LogTypeEnum.Debug, "Sending: " & Data)
        Dim DataBytes() As Byte = ASCIIEncoding.ASCII.GetBytes(Data & vbCrLf)
        Socket.GetStream.Write(DataBytes, 0, DataBytes.Length)
        Socket.GetStream.Flush()

    End Sub

    ''' <summary>
    ''' Handles an incoming IRC command
    ''' </summary>
    ''' <param name="CommandString">The string containing the command and any arguments</param>
    Public Sub OnCommand(ByVal CommandString As String)

        ' Get the Actual Command
        Dim CommandParts() As String = Split(CommandString, Chr(&H20))

        Log(LogTypeEnum.Debug, "Command: " & CommandString)

        Select Case UCase(CommandParts(0))

            Case "NICK" ' Handle the NICK Command

                ' Check if the chosen nickName is in use
                NickAccepted = True
                SyncLock DataStore.ClientStore.Clients
                    For Each Client As ClientDataClass In DataStore.ClientStore.Clients
                        If LCase(Client.NickName) = LCase(Split(CommandString, Chr(&H20))(1)) Then
                            NickAccepted = False
                        End If
                    Next
                End SyncLock

                ' Check if we found a clash
                If Not NickAccepted Then
                    Log(LogTypeEnum.Debug, "Rejecting attempt to use NicnName [" & Split(CommandString, Chr(&H20))(1) & "]")
                    SendString(":" & ServerSettings.ServerName & " 433 * " & Split(CommandString, Chr(&H20))(1) & " :Nickname is already in use.")

                Else ' Nick is not in use

                    ' Save the nickname to this ClientConnection.ClientData
                    ClientData.NickName = Split(CommandString, Chr(&H20))(1)

                    Log(LogTypeEnum.Debug, "Nick Logon: " & Me.ClientData.NickName)

                    ' Send initial ping
                    SendString("PING :12345678")
                    PingTimer.Start()

                End If

            Case "USER" ' Handle the USER Command

                ' Save the USER data to this ClientConnection.ClientData
                ClientData.UserName = Split(CommandString, Chr(&H20))(1)
                ClientData.HostName = Split(CommandString, Chr(&H20))(2).Replace(Chr(&H22), "")
                ClientData.ServerName = Split(CommandString, Chr(&H20))(3).Replace(Chr(&H22), "")
                ClientData.RealName = Split(CommandString, Chr(&H20))(4).Replace(":", "")

                ' Let the main loop know we have received the USER command
                HadUserCommand = True

                Log(LogTypeEnum.Debug, "USER Connected")
                Log(LogTypeEnum.Debug, "    UserName: " & ClientData.UserName)
                Log(LogTypeEnum.Debug, "    HostName: " & ClientData.HostName)
                Log(LogTypeEnum.Debug, "  ServerName: " & ClientData.ServerName)
                Log(LogTypeEnum.Debug, "    RealName: " & ClientData.RealName)

            Case "PONG" ' Handle a PONG command

                Log(LogTypeEnum.Debug, "PONG")

                ' Stop the PING timeout timer on PONG
                PingTimer.Stop()

            Case "JOIN" ' Handle the Join command

                Log(LogTypeEnum.Debug, "Asking to join " & Split(CommandString, Chr(&H20))(1))

                Dim tChan As ChannelDataClass

                Dim ChannelExists As Boolean = False
                SyncLock DataStore.ChannelStore.Channels

                    For Each Channel As ChannelDataClass In DataStore.ChannelStore.Channels
                        If LCase(Channel.Name) = LCase(Split(CommandString, Chr(&H20))(1)) Then
                            ChannelExists = True
                            tChan = Channel
                        End If
                    Next

                    If Not ChannelExists Then
                        tChan = New ChannelDataClass
                        tChan.Name = Split(CommandString, Chr(&H20))(1)
                        tChan.Clients.Add(ClientData.NickName)
                        tChan.Topic = "Temp Topic"
                        tChan.CreatorNickName = ClientData.NickName
                        DataStore.ChannelStore.Channels.Add(tChan)
                        tChan.CreationTimeStamp = Int(DateTime.UtcNow.Subtract(#1/1/1970#).TotalSeconds()).ToString
                    Else
                        tChan.Clients.Add(ClientData.NickName)
                    End If

                    ' Send Channel data
                    SendString(":" & ClientData.NickName & "!" & ClientData.RealName & "@" & ClientData.HostName & " JOIN :" & Split(CommandString, Chr(&H20))(1))
                    SendString(":" & ServerSettings.ServerName & " 332 " & ClientData.NickName & " " & Split(CommandString, Chr(&H20))(1) & " :" & tChan.Topic)
                    SendString(":" & ServerSettings.ServerName & " 333 " & ClientData.NickName & " " & Split(CommandString, Chr(&H20))(1) & " " & tChan.CreatorNickName & " " & tChan.CreationTimeStamp)

                    ' Create a list of channel users
                    Dim Names As New StringBuilder
                    For Each ChanClient As String In tChan.Clients
                        Names.Append(ChanClient & " ")
                    Next

                    ' Tell the client who else is in the channel
                    SendString(":" & ServerSettings.ServerName & " 353 " & ClientData.NickName & " = " & Split(CommandString, Chr(&H20))(1) & " :" & Names.ToString)
                    SendString(":" & ServerSettings.ServerName & " 366 " & ClientData.NickName & " " & Split(CommandString, Chr(&H20))(1) & " :End of /NAMES list.")

                    ' Send the JOIN command to tell any other channel users that we just joined
                    If ChannelExists = True Then
                        For Each ChanClient As String In tChan.Clients
                            Log(LogTypeEnum.Debug, "Telling " & ChanClient & " that " & ClientData.NickName & " joined " & tChan.Name)
                            Dim tClient As ClientDataClass = DataStore.ClientStore.FindClient(ChanClient)
                            If Not LCase(tClient.NickName) = LCase(ClientData.NickName) Then
                                SendString(":" & ClientData.NickName & "!" & ClientData.HostName & " JOIN :" & tChan.Name, tClient.Socket)
                            End If
                        Next
                    End If

                End SyncLock

            Case "MODE"

            Case "PING"

                SendString(":" & ServerSettings.ServerName & " PONG :" & Split(CommandString, Chr(&H20))(1))

            Case "PART"

                Log(LogTypeEnum.Debug, "PART Command: " & CommandString)

                SyncLock DataStore.ChannelStore.Channels
                    For Each Channel As ChannelDataClass In DataStore.ChannelStore.Channels
                        If LCase(Channel.Name) = LCase(Split(CommandString, Chr(&H20))(1)) Then
                            For Each ChanClient As String In Channel.Clients
                                Dim tClient As ClientDataClass = DataStore.ClientStore.FindClient(ChanClient)
                                If Not LCase(tClient.NickName) = LCase(ClientData.NickName) Then
                                    SendString(":" & ClientData.NickName & "!" & ClientData.RealName & "@" & ClientData.HostName & " PART " & LCase(Split(CommandString, Chr(&H20))(1)) & " :", tClient.Socket)
                                End If
                            Next
                            For Each ChanClient As String In Channel.Clients
                                If LCase(ChanClient) = LCase(ClientData.NickName) Then
                                    Channel.Clients.Remove(ChanClient)
                                    Exit For
                                End If
                            Next
                        End If
                    Next
                End SyncLock

            Case "PRIVMSG"

                Dim PrivTarget As String = Split(CommandString, Chr(&H20))(1)
                Dim Message As String = Split(CommandString, ":")(1)

                SyncLock DataStore.ChannelStore.Channels
                    If Left(PrivTarget, 1) = "#" Then
                        For Each Channel As ChannelDataClass In DataStore.ChannelStore.Channels
                            If LCase(Channel.Name) = LCase(Split(CommandString, Chr(&H20))(1)) Then
                                For Each ChanClient As String In Channel.Clients
                                    Dim tClient As ClientDataClass = DataStore.ClientStore.FindClient(ChanClient)
                                    If Not LCase(tClient.NickName) = LCase(ClientData.NickName) Then
                                        SendString(":" & ClientData.NickName & "!" & ClientData.RealName & "@" & ClientData.HostName & " PRIVMSG " & PrivTarget & " :" & Message, tClient.Socket)
                                    End If
                                Next
                            End If
                        Next
                    End If
                End SyncLock

            Case "QUIT" ' Handle the quit command

                ' Remove ourselves from the Client list
                SyncLock DataStore.ClientStore.Clients
                    DataStore.ClientStore.Clients.RemoveAt(ClientStoreIndex)
                End SyncLock

                ' close the connection
                ClientSocket.GetStream.Close()
                ClientSocket.Close()
                Exit Sub

            Case Else ' Handle any other command

                Log(LogTypeEnum.Debug, "Unhandled Command[" & CommandParts(0) & "] " & CommandString)

        End Select

    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    Private Sub DoConnect()

        ConnectDone = True

        ' Send RPL_WELCOME
        SendString(":" & ServerSettings.ServerName & " 001 " & ClientData.NickName & " :Welcome to DotNetIRCD " & ClientData.NickName & "!" & ClientData.UserName & "@" & ClientData.HostName)

        ' Send RPL_YOURHOST
        SendString(":" & ServerSettings.ServerName & " 002 " & ClientData.NickName & " :Your host is " & ServerSettings.ServerName & ", running version DotNetIRCD" & My.Application.Info.Version.ToString)

        ' Send RPL_CREATED
        SendString(":" & ServerSettings.ServerName & " 003 " & ClientData.NickName & " :This server was created " & modMain.ServerStartUpTime.ToString)

        ' Send RPL_MYINFO
        SendString(":" & ServerSettings.ServerName & " 004 " & ClientData.NickName & " " & ServerSettings.ServerName & " DotNetIRCD" & My.Application.Info.Version.ToString & " " & UserModeList & " " & ChannelModeList)

        ' Send RPL_ISUPPORT
        SendString(":" & ServerSettings.ServerName & " 005 " & ClientData.NickName & " " & "CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES SAFELIST HCN MAXCHANNELS=50 CHANLIMIT=#:50 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server")
        SendString(":" & ServerSettings.ServerName & " 005 " & ClientData.NickName & " " & "MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMTGZ NETWORK=" & ServerSettings.NetworkName & " CASEMAPPING=ascii EXTBAN=~,qjncrR ELIST=MNUCT :are supported by this server")
        SendString(":" & ServerSettings.ServerName & " 005 " & ClientData.NickName & " " & "STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server")

        ' Set initial user modes
        ClientData.Modes = "iwx"
        SendString(":" & ClientData.NickName & " MODE " & ClientData.NickName & " :+iwx")

        ' Send MOTD
        SendString(":" & ServerSettings.ServerName & " 422 " & ClientData.NickName & " :MOTD File is missing")

    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    Private Sub SendServerPing()

        'Send a ping and start the timer waiting for a PONG response
        SendString("PING :12345678")
        PingTimer.Start()

    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    Private Sub PingTimer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles PingTimer.Elapsed

        PingTimer.Stop()

        ' Remove ourselves from the Client list
        SyncLock DataStore.ClientStore.Clients
            DataStore.ClientStore.Clients.RemoveAt(ClientStoreIndex)
        End SyncLock

        ' Close the connection
        ClientSocket.GetStream.Close()
        ClientSocket.Close()
        Exit Sub

    End Sub

End Class
